// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using Mono.Cecil;
using AssemblyNameInfo = System.Reflection.Metadata.AssemblyNameInfo;
using TypeName = System.Reflection.Metadata.TypeName;
using TypeNameParseOptions = System.Reflection.Metadata.TypeNameParseOptions;

#nullable enable

namespace Mono.Linker
{
    internal sealed partial class TypeNameResolver
    {
        readonly ITryResolveMetadata _metadataResolver;
        readonly ITryResolveAssemblyName _assemblyResolver;

        private static readonly TypeNameParseOptions s_typeNameParseOptions = new() { MaxNodes = int.MaxValue };

        public readonly record struct TypeResolutionRecord(AssemblyDefinition ReferringAssembly, TypeDefinition ResolvedType);

        public TypeNameResolver(ITryResolveMetadata metadataResolver, ITryResolveAssemblyName assemblyNameResolver)
        {
            _metadataResolver = metadataResolver;
            _assemblyResolver = assemblyNameResolver;
        }

        public bool TryResolveTypeName(
            AssemblyDefinition assembly,
            string typeNameString,
            [NotNullWhen(true)] out TypeReference? typeReference,
            [NotNullWhen(true)] out List<TypeResolutionRecord>? typeResolutionRecords)
        {
            typeResolutionRecords = new List<TypeResolutionRecord>();
            if (!TypeName.TryParse(typeNameString, out TypeName? parsedTypeName, s_typeNameParseOptions))
            {
                typeReference = null;
                return false;
            }
            typeReference = ResolveTypeName(assembly, parsedTypeName, typeResolutionRecords);

            if (typeReference == null)
                typeResolutionRecords = null;

            return typeReference != null;
        }

        TypeReference? ResolveTypeName(AssemblyDefinition originalAssembly, TypeName? typeName, List<TypeResolutionRecord> typeResolutionRecords)
        {
            if (typeName == null)
                return null;

            AssemblyDefinition? assembly = originalAssembly;
            if (typeName.AssemblyName is AssemblyNameInfo assemblyName)
                // In this case we ignore the assembly parameter since the type name has assembly in it
                assembly = _assemblyResolver.TryResolve(assemblyName.Name);

            if (assembly == null)
                return null;

            if (typeName.IsConstructedGenericType)
            {
                var genericTypeRef = ResolveTypeName(assembly, typeName.GetGenericTypeDefinition(), typeResolutionRecords);
                if (genericTypeRef == null)
                    return null;

                Debug.Assert(genericTypeRef is TypeDefinition);
                var genericInstanceType = new GenericInstanceType(genericTypeRef);
                foreach (var arg in typeName.GetGenericArguments())
                {
                    var genericArgument = ResolveTypeName(assembly, arg, typeResolutionRecords);
                    if (genericArgument == null)
                        return null;

                    genericInstanceType.GenericArguments.Add(genericArgument);
                }

                return genericInstanceType;
            }
            else if (typeName.IsArray || typeName.IsPointer || typeName.IsByRef)
            {
                var elementType = ResolveTypeName(assembly, typeName.GetElementType(), typeResolutionRecords);
                if (elementType == null)
                    return null;

                if (typeName.IsArray)
                    return typeName.IsSZArray ? new ArrayType(elementType) : new ArrayType(elementType, typeName.GetArrayRank());
                if (typeName.IsByRef)
                    return new ByReferenceType(elementType);
                if (typeName.IsPointer)
                    return new PointerType(elementType);
                Debug.Fail("Unreachable");
                return null;
            }

            Debug.Assert(typeName.IsSimple);
            TypeName topLevelTypeName = typeName;
            while (topLevelTypeName.IsNested)
                topLevelTypeName = topLevelTypeName.DeclaringType;
            Debug.Assert(topLevelTypeName.AssemblyName == typeName.AssemblyName);
            TypeDefinition? resolvedType = GetSimpleTypeFromModule(typeName, assembly.MainModule);

            // True type references (like generics and arrays) don't count as actually resolved types, they're just wrappers
            // so only record type resolutions for types which are actually resolved.
            if (resolvedType != null)
            {
                typeResolutionRecords.Add(new(assembly, resolvedType));
                return resolvedType;
            }

            // If it didn't resolve and wasn't assembly-qualified, we also try core library
            var coreLibrary = _metadataResolver.TryResolve(originalAssembly.MainModule.TypeSystem.Object)?.Module.Assembly;
            if (coreLibrary is null)
                return null;

            if (topLevelTypeName.AssemblyName == null && assembly != coreLibrary)
            {
                resolvedType = GetSimpleTypeFromModule(typeName, coreLibrary.MainModule);
                if (resolvedType != null)
                {
                    typeResolutionRecords.Add(new(coreLibrary, resolvedType));
                    return resolvedType;
                }
            }

            return null;

            TypeDefinition? GetSimpleTypeFromModule(TypeName typeName, ModuleDefinition module)
            {
                if (typeName.IsNested)
                {
                    TypeDefinition? type = GetSimpleTypeFromModule(typeName.DeclaringType, module);
                    if (type == null)
                        return null;
                    return GetNestedType(type, TypeName.Unescape(typeName.Name));
                }

                return module.ResolveType(TypeName.Unescape(typeName.FullName), _metadataResolver);
            }

            TypeDefinition? GetNestedType(TypeDefinition type, string nestedTypeName)
            {
                if (!type.HasNestedTypes)
                    return null;

                foreach (var nestedType in type.NestedTypes)
                {
                    if (nestedType.Name == nestedTypeName)
                        return nestedType;
                }

                return null;
            }
        }
    }
}
